#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <vector>
#include <float.h>

#pragma warning(disable: 4996)

#include "..\glew\glew.h"
#include "..\glut\glut.h"

#include "..\xobjIO\XobjIO.h"
#include "..\xobjIO\glMesh.h"
#include "..\xobjIO\glWorld.h"

#include "..\cuCommon\cuTypes.h"

#include "..\rtCommon\RegularGrid.h"
#include "..\rtCommon\C_Camera.h"
#include "..\rtCommon\Plane.h"
#include "..\rtCommon\Frustum.h"
#include "..\rtCommon\BoolDebug.h"
#include "..\rtCommon\trace.h"

#include "..\gui\C_Mouse.h"

#include "..\glCommon\glCommon_string.h"
#include "..\glCommon\glCommon.h"
#include "..\glCommon\glShader.h"
#include "..\glCommon\glProgram.h"
#include "..\glCommon\glRender.h"

SBoolDebug g_bDebug;

std::vector<const char *> g_vPrintLog;
std::vector<CXobjIO *>    g_vpXobj;
std::vector<CglMesh *>    g_vpMesh;
CglMesh *                 g_pMesh = NULL;

std::vector<CglShader  *> g_vpVxShader;
std::vector<CglShader  *> g_vpFtShader;
std::vector<CglProgram *> g_vpGlProgram;

CglWorld g_World;

C_Camera g_Camera;
C_Mouse  g_Mouse;

int g_nShowTreeMaxLevel = 3;
const bool g_PrebuildTree = false;

float3 g_RayO;
float3 g_RayD;
int    g_RayI;

//////////////////////////////////////////////////////////////////////////
/// ray-tracing variables
//////////////////////////////////////////////////////////////////////////
Sf4Grid   g_gRayO;
Sf4Grid   g_gRayD;
sf_Buffer g_bTmin;
si2Buffer g_bID;
Suc4Grid  g_Rgba8_2x2;
Suc4Grid  g_Rgba8_1x1;

//////////////////////////////////////////////////////////////////////////
/// Gfx_n namespace holds window properties
/// Gfx_n is an easy replacement for a window class
//////////////////////////////////////////////////////////////////////////
namespace Gfx_n
{
	int2 Resolution = { 256, 256 };
	int2 Position   = {   0,   0 };
	int2 Sample     = {   1,   1 } ;

	uint32 nFlags = GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH;

	int32 nId = 0;

	bool bFullscreen = false;
};

bool ParseCommandLine(int argc, char *argv[])
{
	bool error = false;
	for (int iarg = 0; iarg < argc; iarg++)
	{
		if (strcmp(argv[iarg], GLUT_STRING_RESOLUTION) == 0)
		{
			if ( iarg+2 > argc ) 
			{
				fprintf(stderr, "Error: Invalid command line parameters, missing resolution values\n");
				error = true;
				break;
			}
			else
			{
				Gfx_n::Resolution.x = atoi(argv[iarg+1]);
				Gfx_n::Resolution.y = atoi(argv[iarg+2]);
			}
		}
		if (strcmp(argv[iarg], GLUT_STRING_POSITION) == 0)
		{
			if ( iarg+2 > argc ) 
			{
				fprintf(stderr, "Error: Invalid comand line parameters, missing position values\n");
				error = false;
				break;
			}
			else
			{
				Gfx_n::Position.x = atoi(argv[iarg+1]);
				Gfx_n::Position.y = atoi(argv[iarg+2]);
			}
		}
		if (strcmp(argv[iarg], GLUT_STRING_FULLSCREEN) == 0)
		{
			Gfx_n::bFullscreen = true;
		}
	}
	return error;
}


// Idle() is called every time in the message processing loop. It is the main callback function 
void Idle(void);
// Display() is called whenever window content is invalidated
void Display(void);
// Keyboard() is called whenever any of the character keys are pressed
void Keyboard(uint8 key, int32 mousex, int32 mousey);
// KeyboardUp() is called whenever any of the pressed character keys is released
void KeyboardUp(uint8 key, int32 mousex, int32 mousey);
// Special() is called whenever any of the special (F1-F12, etc) keys are pressed
void Special(int32 key, int32 x, int32 y);
// SpecialUp() is called whenever any of the pressed special (F1-F12, etc) keys is released
void SpecialUp(int32 key, int32 x, int32 y);

// Reshape() is called when window size is changed
void Reshape(int32 w, int32 h);

// Functions that process mouse behavior
// Mouse() is called whenever user clicks any button
void Mouse(int32 button, int32 state, int32 mousex, int32 mousey);
// Motion() is called whenever user holds any of the buttons while moving mouse
void Motion(int32 mousex, int32 mousey);
// PassiveMotion() is called whenever mouse moves and no buttons are pressed
void PassiveMotion(int32 mousex, int32 mousey);
// Entry() is called when mouse cursor enters / leaves current window
void Entry(int32 state);

int main(int argc, char *argv[])
{
	ParseCommandLine(argc, argv);

	glutInit( &argc, argv );   

	glutInitWindowSize     ( Gfx_n::Resolution.x, Gfx_n::Resolution.y );    
	glutInitWindowPosition ( Gfx_n::Position.x,   Gfx_n::Position.y   );
	glutInitDisplayMode    ( Gfx_n::nFlags                            );

	Gfx_n::nId = glutCreateWindow("ray tracing test window");

	GLenum gl_error = glewInit();
	if (gl_error != GLEW_OK)
	{
		char glew_error[256];
		sprintf(glew_error, "Error: %s ", glewGetErrorString(gl_error));
		g_vPrintLog.push_back(glew_error);
	}
	else
	{
		char glew_status[256];
		sprintf(glew_status, "Status: Using GLEW %s ", glewGetString(GLEW_VERSION));
		g_vPrintLog.push_back(glew_status);
	}

	glutDisplayFunc    ( Display  );
	glutIdleFunc       ( Idle     );
	glutKeyboardFunc   ( Keyboard );
	glutSpecialFunc    ( Special  );
	glutReshapeFunc    ( Reshape  );
	glutMouseFunc      ( Mouse    );
	glutMotionFunc     ( Motion   );

	if (Gfx_n::bFullscreen)
		glutFullScreen();  

	char * fname  = "..\\Media\\models\\kdtree_spheres3.XOBJ";
	//char * fname  = "..\\Media\\models\\cubes.XOBJ";
	//char * fname = "..\\Media\\models\\plane.XOBJ";
	//char * fname = "..\\Media\\models\\sphere2.OBJ";
	//char * fname = "..\\Media\\models\\pyramid.XOBJ";
	//char * fname = "..\\Media\\models\\teapot.XOBJ";
	//char * fname = "..\\Media\\models\\complex_scene.xobj";
	if (CXobjIO::Load(g_vpXobj, fname ) != XOBJ_OK)
	{
		char xobj_error[512];
		sprintf(xobj_error, "Error: %s not found ", fname);
		g_vPrintLog.push_back(xobj_error);
	}
	else
	{
		char xobj_error[512];
		sprintf(xobj_error, "File %s loaded successfully. g_vpXobj.size() = %d ", fname, g_vpXobj.size());

		uint32 faceCount = 0;	

		g_World.Create(&g_vpXobj[0], g_vpXobj.size());

		g_pMesh = g_World.pMesh();

		g_World.Update(BUFFER_R);

		for (uint32 iobj = 0; iobj < g_vpXobj.size(); iobj++)
		{
			CglMesh * pMesh = &g_pMesh[iobj];

			g_vpMesh.push_back(pMesh);

			faceCount += g_vpXobj[iobj]->FaceCount();
		}

		printf("Number of faces per scene : %d\n", faceCount); 

		g_vPrintLog.push_back(xobj_error);
	}

	char * vs_fname[] = { "..\\Media\\shaders\\simple.vs" };
	char * ps_fname[] = { "..\\Media\\shaders\\simple.ps" };
	CglShader *pShader = NULL; 

	pShader = new CglShader();
	pShader->CreateFromFile(vs_fname[0], GL_VERTEX_SHADER);
	g_vPrintLog.push_back(pShader->m_Log);
	g_vpVxShader.push_back(pShader);

	pShader = new CglShader();
	pShader->CreateFromFile(ps_fname[0], GL_FRAGMENT_SHADER);
	g_vPrintLog.push_back(pShader->m_Log);
	g_vpFtShader.push_back(pShader);

	CglProgram *pProgram = new CglProgram();

	pProgram->Create(g_vpVxShader[0]->Id(), g_vpFtShader[0]->Id());
	g_vPrintLog.push_back(pProgram->m_Log);

	g_vpGlProgram.push_back(pProgram);

	float asp = Gfx_n::Resolution.x / (float) Gfx_n::Resolution.y;

	g_Camera.Asp(asp);
	g_Camera.Eye(0, 1, 58);
	g_Camera.Dir(0, 0,-1);

	glEnable (GL_DEPTH_TEST);
	glDisable(GL_CULL_FACE);

	if (g_PrebuildTree)
	{
		g_World.TransformWS   (EXEC_H);
		g_World.PrimitiveAABB (EXEC_H);
		g_World.SceneAABB     (EXEC_H);
		g_World.KdTree        (EXEC_H);
		g_World.BVHTree       (EXEC_H);
	}

	glutMainLoop();

	return 0;
}

void Reshape(int32 w, int32 h)
{
	float zn = g_Camera.zN();
	float zf = g_Camera.zF();
	float asp = w / (float) h;
	float fov = g_Camera.FoV();

	glViewport(0, 0, w, h);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(fov, asp, zn, zf);

	g_Camera.Asp(asp); // update aspect ratio variable

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	Gfx_n::Resolution.x = w;
	Gfx_n::Resolution.y = h;

	int2 ssaa = Gfx_n::Resolution * Gfx_n::Sample;

	g_gRayO.Release ( BUFFER_H_AND_D );
	g_gRayD.Release ( BUFFER_H_AND_D );
	g_Rgba8_1x1.Release( BUFFER_H_AND_D );
	g_Rgba8_2x2.Release( BUFFER_H_AND_D );
	g_bTmin.Release ( BUFFER_H_AND_D );
	g_bID.Release   ( BUFFER_H_AND_D );

	g_Rgba8_1x1.Create( w, h, 1, BUFFER_H_AND_D );
	g_Rgba8_2x2.Create( ssaa.x, ssaa.y, 1, BUFFER_H_AND_D );
	g_gRayO.Create ( ssaa.x, ssaa.y, 1, BUFFER_H_AND_D );
	g_gRayD.Create ( ssaa.x, ssaa.y, 1, BUFFER_H_AND_D );
	g_bTmin.Create ( ssaa.x * ssaa.y,   BUFFER_H_AND_D );
	g_bID.Create   ( ssaa.x * ssaa.y,   BUFFER_H_AND_D );
}

void Display(void)
{
	float3 at  = g_Camera.At();
	float3 eye = g_Camera.Eye();
	float3 up  = g_Camera.Up();	
	
	float  zn  = g_Camera.zN();
	float  zf  = g_Camera.zF();
	float  asp = g_Camera.Asp();
	float  fov = g_Camera.FoV();

	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(fov, asp, zn, zf);

	glLookAt(eye, at, up);

	g_World.TransformWS(EXEC_H);

	g_World.Update(BUFFER_W);

	if (g_bDebug.bRayTrace)
	{
		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		glLoadIdentity();

		glMatrixMode(GL_PROJECTION);
		glPushMatrix();
		glLoadIdentity();

		glRasterPos2f(-1, -1);
		uchar4 * pRgba8_1x1 = g_Rgba8_1x1.getBufferPtr()->hPtrR();
		uchar4 * pRgba8_2x2 = g_Rgba8_2x2.getBufferPtr()->hPtrR();

		mpResolveBuffer_v4(Gfx_n::Resolution, Gfx_n::Sample, RT_LINEAR, pRgba8_2x2, pRgba8_1x1);

		glDrawPixels(Gfx_n::Resolution.x, Gfx_n::Resolution.y, GL_RGBA, GL_UNSIGNED_BYTE, pRgba8_1x1);

		glMatrixMode(GL_MODELVIEW);
		glPopMatrix();

		glMatrixMode(GL_PROJECTION);
		glPopMatrix();

		glCheckError(__LINE__, "Raytracing.cpp");
	}
	else 
	{

		//////////////////////////////////////////////////////////////////////////
		/// if the tree wasn't prebuild
		//////////////////////////////////////////////////////////////////////////
		if (!g_PrebuildTree)
		{
			g_World.PrimitiveAABB (EXEC_H);
			g_World.SceneAABB     (EXEC_H);

			//////////////////////////////////////////////////////////////////////////
			/// build appropriate tree
			//////////////////////////////////////////////////////////////////////////
			if (g_bDebug.bKdTree)  g_World.KdTree  (EXEC_H);
			if (g_bDebug.bBVHTree) g_World.BVHTree (EXEC_H);
		}

		//////////////////////////////////////////////////////////////////////////
		/// if render tree - render appropriate tree at max level
		//////////////////////////////////////////////////////////////////////////
		if (g_bDebug.bRenderTree && g_bDebug.bKdTree)  g_World.RenderKdTree(g_nShowTreeMaxLevel, true);
		if (g_bDebug.bRenderTree && g_bDebug.bBVHTree) g_World.RenderBVHTree(g_nShowTreeMaxLevel, true);

		//////////////////////////////////////////////////////////////////////////
		/// if render leaf geometry - render appropriate leaf at max level
		//////////////////////////////////////////////////////////////////////////
		if (g_bDebug.bRenderLeafGeometry && g_bDebug.bKdTree)  g_World.RenderKdTreeLeaf(g_nShowTreeMaxLevel, true);
		if (g_bDebug.bRenderLeafGeometry && g_bDebug.bBVHTree) g_World.RenderBVHTreeLeaf(g_nShowTreeMaxLevel, true);

		//////////////////////////////////////////////////////////////////////////
		/// if trace single ray - trace one and render its path
		//////////////////////////////////////////////////////////////////////////
		if (g_bDebug.bTraceSinglRay && g_bDebug.bKdTree)  g_World.RenderKdTreeRayTrace(g_RayO, g_RayD, g_nShowTreeMaxLevel, false);
		if (g_bDebug.bTraceSinglRay && g_bDebug.bBVHTree) g_World.RenderBVHTreeRayTrace(g_RayO, g_RayD, g_nShowTreeMaxLevel, false);

		//////////////////////////////////////////////////////////////////////////
		/// if no tree structure was chosen - ignore debug rendering
		//////////////////////////////////////////////////////////////////////////
		if (!g_bDebug.bKdTree && !g_bDebug.bBVHTree) glRender( &g_World );
	}

	float4 fontColor = {1.0f, 1.0f, 0.0f, 1.0f};
	//glPrintf(-1.0f, 1.0f - ((20 + 30.0f) / Gfx_n::Resolution.y), &fontColor, GLUT_BITMAP_9_BY_15, "FPS = %f", glFPS(true, 100));

#if 0
	for (uint32 ilog = 0; ilog < g_vPrintLog.size(); ilog++)
		glPrintf(-1.0f, 1.0f - ((20 + 30.0f * (2 + ilog)) / Gfx_n::Resolution.y), &fontColor, GLUT_BITMAP_9_BY_15, g_vPrintLog[ilog]);
#endif

	glColor3f(1.0f, 1.0f, 0.0f);
	
	glCheckError(__LINE__, "Raytracing.cpp");
	glutSwapBuffers();
}

void Idle(void)
{
	Display();

	if (g_bDebug.bRayTrace)
	{	
		sf4Buffer *pRayO = g_gRayO.getBufferPtr();
		sf4Buffer *pRayD = g_gRayD.getBufferPtr();
		uc4Buffer *pRgba8_1x1 = g_Rgba8_1x1.getBufferPtr();
		uc4Buffer *pRgba8_2x2 = g_Rgba8_2x2.getBufferPtr();
		
		uchar4 red = { 0, 0, 0, 255 };
		float f = FLT_MAX;

		g_bTmin.Memset(&f);
		pRgba8_2x2->Memset(&red);

		mpGenerateEyeRays(g_Camera, 
											Gfx_n::Resolution, 
											Gfx_n::Sample, 
											RT_PERSPECTIVE, 
											pRayO->hPtrR(), 
											pRayD->hPtrR());

		if (g_bDebug.bKdTree)
		{
			if (!g_PrebuildTree) g_World.KdTree(EXEC_H);

			g_World.KdTrace(pRayO[0], 
											pRayD[0], 
											g_bTmin, 
											g_bID, 
											pRgba8_2x2[0]);

			if (g_bDebug.bTraceSinglRay)
				g_World.KdTrace(g_RayO, 
												g_RayD, 
												g_RayI, 
												g_bTmin, 
												g_bID, 
												pRgba8_2x2[0]);
		}

		if (g_bDebug.bBVHTree)
		{
			if (!g_PrebuildTree) g_World.BVHTree(EXEC_H);

			g_World.BVHTrace(pRayO[0], 
											 pRayD[0], 
											 g_bTmin, 
											 g_bID, 
											 pRgba8_2x2[0]);

			if (g_bDebug.bTraceSinglRay)				
				g_World.BVHTrace(g_RayO, 
												 g_RayD, 
												 g_RayI, 
												 g_bTmin, 
												 g_bID, 
												 pRgba8_2x2[0]);

		}
	}
}

void Keyboard(uint8 key, int32 mousex, int32 mousey)
{
	switch (key)
	{
	case GLUT_KEY_ESC: 
		// any opengl resource clean-up should go here
		g_World.Release();
		glutDestroyWindow(Gfx_n::nId);
		// any cpu resource clean-up should go here
		exit(0);
		break;

		//////////////////////////////////////////////////////////////////////////
		/// tree data structure debug rendering toggle on/off
		/// if (RenderTree is true) render bounding boxes of a
		///		if (Kdtree  is true) KdTree 
		///		if (BVHTree is true) BVHTree 
		/// at level specified in RenderXXXTree call
		//////////////////////////////////////////////////////////////////////////
		//////////////////////////////////////////////////////////////////////////
	case '3':
		g_bDebug.bRenderTree = !g_bDebug.bRenderTree;
		break;

		//////////////////////////////////////////////////////////////////////////
		/// if (RenderTree is true) generate ray at mouse click and trace using
		///		if (Kdtree  is true) KdTree  data structure
		///		if (BVHTree is true) BVHTree data structure 
		/// an render passing levels of a tree
		//////////////////////////////////////////////////////////////////////////
	case '4':
		g_bDebug.bTraceSinglRay = !g_bDebug.bTraceSinglRay;
		break;

		//////////////////////////////////////////////////////////////////////////
		/// if (RenderTree is true) render leaf geometry of a
		///		if (Kdtree  is true) KdTree 
		///		if (BVHTree is true) BVHTree 
		/// at level specified in RenderXXXTreeLeaf call
		//////////////////////////////////////////////////////////////////////////
	case '5':
		g_bDebug.bRenderLeafGeometry = !g_bDebug.bRenderLeafGeometry;
		break;

		//////////////////////////////////////////////////////////////////////////
		/// choose between KdTree and BVHTree tracing
		//////////////////////////////////////////////////////////////////////////
	case '6':
		g_bDebug.bKdTree = !g_bDebug.bKdTree;
		if (g_bDebug.bKdTree) g_bDebug.bBVHTree = false;
		break;
	case '7':
		g_bDebug.bBVHTree = !g_bDebug.bBVHTree;
		if (g_bDebug.bBVHTree) g_bDebug.bKdTree = false;
		break;

	case '8':
		g_bDebug.bCUDA = !g_bDebug.bCUDA;
		break;

		//////////////////////////////////////////////////////////////////////////
		/// run ray tracing code path
		//////////////////////////////////////////////////////////////////////////
	case '9':
		g_bDebug.bRayTrace = !g_bDebug.bRayTrace;
		break;

	case '=': g_nShowTreeMaxLevel++; break;
	case '-': g_nShowTreeMaxLevel--; if (g_nShowTreeMaxLevel < 0) g_nShowTreeMaxLevel = 0; break;

	default:
		break;
	}
}

void Special(int32 key, int32 mousex, int32 mousey)
{
	switch (key)
	{
	case GLUT_KEY_LEFT:
		g_Camera.Update((uint32) e_StrafeLeft, 0.5f);
		break;
	case GLUT_KEY_RIGHT:
		g_Camera.Update((uint32) e_StrafeRight, 0.5f);
		break;
	case GLUT_KEY_UP:
		g_Camera.Update((uint32) e_StepForward, 0.5f);
		break;
	case GLUT_KEY_DOWN:
		g_Camera.Update((uint32) e_StepBackward, 0.5f);
		break;

	default:
		break;
	}
}

void KeyboardUp(uint8 key, int32 mousex, int32 mousey)
{
	//switch (key)
	//{
	//    default:
	//    break;
	//}
}

void SpecialUp(int32 key, int32 mousex, int32 mousey)
{
	//switch (key)
	//{
	//    default:
	//    break;
	//}
}

void Mouse(int32 button, int32 state, int32 mousex, int32 mousey)
{
	g_Mouse.Update(mousex, mousey, button, !state);

	if (button == GLUT_RIGHT_BUTTON)
	{
		mpGenerateEyeRay(g_Camera, Gfx_n::Resolution, RT_PERSPECTIVE, mousex, Gfx_n::Resolution.y - mousey, g_RayO, g_RayD, g_RayI);

		printf("ray %d \n", g_RayI);
	}
}
void Motion(int32 mousex, int32 mousey)
{
	g_Mouse.Update(mousex, mousey);

	if (g_Mouse.Pressed( (int32) e_Left))
	{
		int dx = g_Mouse.currX() - g_Mouse.prevX();
		int dy = g_Mouse.currY() - g_Mouse.prevY();

		g_Camera.Update(dx * 0.001f, -dy * 0.001f);
	}
}
